package pri.hillchen.std.consurrent.util;

import org.apache.http.*;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.*;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.client.LaxRedirectStrategy;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.AbstractHttpMessage;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HttpContext;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.util.EntityUtils;
import org.apache.log4j.Logger;
import pri.hillchen.std.consurrent.util.handler.AbstractResponseHandler;
import pri.hillchen.std.consurrent.util.post.MapPostRequestParamsEntityFactory;
import pri.hillchen.std.consurrent.util.post.RequestParamsEntityFactory;

import javax.net.ssl.SSLException;
import java.io.*;
import java.net.*;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * Created by hillchen on 2018/2/10.
 * httpclient管理工具
 */
public class HttpclientManager {
    private static Logger LOGGER = Logger.getLogger(HttpclientManager.class);

    public static final long RELEASE_CONNECTION_WAIT_TIME = 5000;// 监控连接间隔
    /**
     * 连接池请求管理器
     */
    private static PoolingHttpClientConnectionManager httpClientConnectionManager = null;
    /**
     *定义重定向策略
     */
    private static LaxRedirectStrategy redirectStrategy = null;
    /**
     * 定义请求重试连接策略
     */
    private static HttpRequestRetryHandler myRetryHandler = null;
    /**
     * 定义https socket连接工厂类
     */
    private static SSLConnectionSocketFactory sslConnectionSocketFactory = null;

    private static int reTryCount = 3;

    /**
     * 连接池最大请求连接数
     */
    private static int maxTot = 100;
    /**
     * 连接池每一路由(同一请求处理服务器)最大请求连接数
     */
    private static int maxPerRoute = 20;

    /**
     * 默认请求连接超时时间
     */
    private static int defConnectTimeout = 3*1000;
    /**
     * 默认请求socketTime连接时间(请求处理和请求响应时间?)
     */
    private static int defSocketTimeout = 3*1000;
    /**
     * 连接池中等待获取连接时间
     */
    private static int defConnectionRequestTimeout = 6*1000;

    static {
        initHttpClient();
        // 启动清理连接池链接线程
        Thread idleConnectionMonitorThread = new IdleConnectionMonitorThread(httpClientConnectionManager);
        idleConnectionMonitorThread.setDaemon(true);
        idleConnectionMonitorThread.start();
    }

    /**
     * 创建String请求响应处理器
     * @return
     */
    private static ResponseHandler<String> createStringResponseHandler(){
        return new AbstractResponseHandler<String>() {
            @Override
            public String succHandle(HttpResponse response) throws IOException {
                return EntityUtils.toString(response.getEntity(), "UTF-8");
            }
        };
    }

    /**
     * 创建重试处理策略
     * @param reTryCount
     * @return
     */
    private static HttpRequestRetryHandler createHttpRequestRetryHandler(final int reTryCount){
        return new HttpRequestRetryHandler() {
            @Override
            public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
                if (executionCount >= reTryCount) {
                    return false;
                }
                if (exception instanceof InterruptedIOException) {
                    return false;
                }
                if (exception instanceof UnknownHostException) {
                    return false;
                }
                if (exception instanceof ConnectTimeoutException) {
                    return false;
                }
                if (exception instanceof SSLException) {
                    return false;
                }
                HttpClientContext clientContext = HttpClientContext.adapt(context);
                HttpRequest request = clientContext.getRequest();
                boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);
                if (idempotent) {
                    // Retry if the request is considered idempotent
                    return true;
                }
                return false;
            }
        };
    }
    private static SSLContextBuilder createSSLContextBuilder() throws KeyStoreException, NoSuchAlgorithmException {
        SSLContextBuilder builder = new SSLContextBuilder();
        builder.loadTrustMaterial(null, new TrustSelfSignedStrategy());
        return builder;
    }
    /**
     * 初始化请求连接池管理器
     */
    public static void initHttpClient() {
        try {
            // 重定向策略初始化
            redirectStrategy = new LaxRedirectStrategy();
            // 请求重试机制，默认重试3次
            myRetryHandler = createHttpRequestRetryHandler(reTryCount);

            sslConnectionSocketFactory = new SSLConnectionSocketFactory(createSSLContextBuilder().build(), NoopHostnameVerifier.INSTANCE);
            Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
                    .register("http", new PlainConnectionSocketFactory())//
                    .register("https", sslConnectionSocketFactory)//
                    .build();
            // 创建httpclient连接池
            httpClientConnectionManager = new PoolingHttpClientConnectionManager(registry);
            // 设置连接池最大数量,这个参数表示所有连接最大数。
            httpClientConnectionManager.setMaxTotal(maxTot);
            // 设置单个路由最大连接数量，表示单个域名的最大连接数，
            // 例如:www.baidu.com.www.google.com表示不同的域名,则连接统一域名下的资源的最大连接数就是该参数,总和是上面的参数。
            httpClientConnectionManager.setDefaultMaxPerRoute(maxPerRoute);
        } catch (Exception e) {
            LOGGER.error("初始化httpclient连接池失败.", e);
        }

    }

    /**
     * 创建默认请求连接配置参数
     * @return
     */
    private static RequestConfig createDefaultRequestConfig(){
        return  RequestConfig.custom()//
                .setConnectTimeout(defConnectTimeout)//
                .setSocketTimeout(defSocketTimeout)//
                // 忽略cookie,如果不需要登陆最好去掉,否则修改策略保存cookie即可
                .setCookieSpec(CookieSpecs.IGNORE_COOKIES)//
                .setConnectionRequestTimeout(defConnectionRequestTimeout)//
                // .setProxy(ip)//设置代理ip,不设置就用本机
                .build();

    }

    public static CloseableHttpClient getHttpClient() {
        return getHttpClient(createDefaultRequestConfig());
    }

    public static CloseableHttpClient getHttpClient(RequestConfig requestConfig) {

        // 连接池配置
        CloseableHttpClient httpClient = HttpClients.custom()//
                .setSSLSocketFactory(sslConnectionSocketFactory)//
                .setConnectionManager(httpClientConnectionManager)//
                .setDefaultRequestConfig(requestConfig)//
                .setRedirectStrategy(redirectStrategy)//
                .setRetryHandler(myRetryHandler)//
                .build();

        return httpClient;
    }

    private static HttpRequestBase createHttpRequestBase(String urlString,String methd,Map<String, String> headers) throws MalformedURLException, URISyntaxException {
        HttpRequestBase httpRequestBase = null;
        URI uri = createUri(urlString);
        if(methd.equalsIgnoreCase("GET")){
            httpRequestBase = new HttpGet(uri);
        }else if(methd.equalsIgnoreCase("POST")){
            httpRequestBase = new HttpPost(uri);
        }else if(methd.equalsIgnoreCase("HEAD")){
            httpRequestBase = new HttpHead(uri);
        }else{
            String errorMsg = String.format("请求方法%s异常，暂时只支持GET和POST请求!",methd);
            LOGGER.error(errorMsg);
            throw new RuntimeException(errorMsg);
        }
        setCommonHeaders(httpRequestBase);  // 额外的header
        if (headers != null && !headers.isEmpty()) {
            for (Map.Entry<String, String> header : headers.entrySet()) {
                if (httpRequestBase.containsHeader(header.getKey())) {
                    httpRequestBase.setHeader(header.getKey(), header.getValue());
                } else {
                    httpRequestBase.addHeader(header.getKey(), header.getValue());
                }
            }
        }
        return httpRequestBase;
    }

    private static long getConectConentLength(String urlString,Map<String, String> headerParams) throws Exception{
        long contentLength = -1;
        HttpRequestBase httpRequestBase = createHttpRequestBase(urlString,"HEAD",headerParams);
        HttpClient httpClient = getHttpClient();
        HttpResponse httpResponse = httpClient.execute(httpRequestBase);
        int statusCode = httpResponse.getStatusLine().getStatusCode();

        if(statusCode != 200){
            throw new RuntimeException("资源不存在!");
        }
        Header[] headers = httpResponse.getHeaders("Content-Length");
        if(headers.length > 0){
            contentLength = Long.valueOf(headers[0].getValue());
        }
        httpRequestBase.abort();
        return contentLength;
    }

    private static long getConectConentLength(String urlString) throws Exception{
        return getConectConentLength(urlString,Collections.emptyMap());
    }

    private static boolean checkUrl(String urlString ){
        if (null == urlString || urlString.isEmpty() || !urlString.startsWith("http")) {
            return false;
        }
        return true;
    }

    private static URI createUri(String urlString) throws MalformedURLException, URISyntaxException {
        URL url = new URL(urlString);
        return new URI(url.getProtocol(), url.getHost(), url.getPath(), url.getQuery(), null);
    }

    public static <T>T requestGetUrlContent( String urlString , Map<String, String> headers, ResponseHandler<T> responseHandler ) {
        urlString = urlString.trim();
        if (!checkUrl(urlString)) {
            return null;
        }
        HttpRequestBase httpRequestBase = null;
        // 转化String url为URI,解决url中包含特殊字符的情况
        try {
            httpRequestBase = createHttpRequestBase(urlString,"GET",headers);

            HttpClient httpClient = getHttpClient();
            return httpClient.execute(httpRequestBase, responseHandler);
        } catch (Exception e) {
            LOGGER.error("请求错误·url=" + urlString,e);
            return null;
        } finally {
            httpRequestBase.abort();
        }
    }
    public static <T>T requestPostUrlContent( String urlString , Map<String, String> headers, ResponseHandler<T> responseHandler ) {
        return requestPostUrlContent(urlString,headers,Collections.emptyMap(),responseHandler);
    }

    public static <T>T requestPostUrlContent( String urlString , Map<String, String> headers, Map<String, String> params, ResponseHandler<T> responseHandler ) {
        return requestPostUrlContent(urlString,headers,new MapPostRequestParamsEntityFactory(params),responseHandler);
    }


    public static <T>T requestPostUrlContent(String urlString , Map<String, String> headers, RequestParamsEntityFactory requestParamsEntityFactory, ResponseHandler<T> responseHandler ) {
        urlString = urlString.trim();
        if (!checkUrl(urlString)) {
            return null;
        }
        HttpRequestBase httpRequestBase = null;
        // 转化String url为URI,解决url中包含特殊字符的情况
        try {
            httpRequestBase = createHttpRequestBase(urlString,"POST",headers);
            HttpPost httpPost = (HttpPost) httpRequestBase;
            HttpEntity httpEntity = requestParamsEntityFactory.createRequestParamsEntity();
            if(httpEntity != null){
                httpPost.setEntity(httpEntity);
            }
            HttpClient httpClient = getHttpClient();
            return httpClient.execute(httpRequestBase, responseHandler);
        } catch (Exception e) {
            LOGGER.error("请求错误·url=" + urlString);
            return null;
        } finally {
            httpRequestBase.abort();
        }
    }
    public static String requestStringGetUrlContent(String urlString) {
        return requestStringGetUrlContent(urlString,Collections.emptyMap());
    }
    /**
     * get请求 headers表示特殊的请求头
     */
    public static String requestStringGetUrlContent(String urlString, Map<String, String> headers) {
        String result = requestGetUrlContent(urlString,headers,createStringResponseHandler());
        return result == null ? "" : result;
    }

    /**
     * get请求 headers表示特殊的请求头
     */
    public static String requestStringPostUrlContent(String urlString, Map<String, String> headers) {
        String result = requestPostUrlContent(urlString,headers,createStringResponseHandler());
        return result == null ? "" : result;
    }

    private static void setCommonHeaders(AbstractHttpMessage request) {
        request.addHeader("Accept",
                "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8");
        // request.addHeader("Connection", "keep-alive");
        request.addHeader("Accept-Language", "zh-CN,zh;q=0.8");
        request.addHeader("Accept-Encoding", "gzip, deflate, br");
        // User-Agent最好随机换
        request.addHeader("User-Agent",
                "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36");
    }

    /**
     * 连接处理,释放连接池连接
     *
     */
    public static class IdleConnectionMonitorThread extends Thread {

        private static volatile boolean shutdown = false;
        private PoolingHttpClientConnectionManager poolingHttpClientConnectionManager;

        public IdleConnectionMonitorThread(PoolingHttpClientConnectionManager poolingHttpClientConnectionManager) {
            this.poolingHttpClientConnectionManager = poolingHttpClientConnectionManager;
        }

        @Override
        public void run() {
            try {
                while (!shutdown) {
                    synchronized (IdleConnectionMonitorThread.class) {
                        wait(RELEASE_CONNECTION_WAIT_TIME);
                        // Close expired connections
                        poolingHttpClientConnectionManager.closeExpiredConnections();
                        // that have been idle longer than 30 sec
                        poolingHttpClientConnectionManager.closeIdleConnections(2, TimeUnit.MINUTES);
                    }
                }
            } catch (InterruptedException ex) {
                LOGGER.error("释放连接池连接出错.");
            }
        }

        public void shutdown() {
            shutdown = true;
            synchronized (IdleConnectionMonitorThread.class) {
                notifyAll();
            }
        }

    }

    private static String createRangeVal(long startPosition,long endPosition){
        return String.format("bytes=%s-%s",startPosition,endPosition);
    }

    public static void sliceDown(final String url,String fileName) throws Exception {
        long defSliceCount = 100*1024;
        long fileLength = getConectConentLength(url);
        int spliceCount = (int)(fileLength/defSliceCount + (fileLength%defSliceCount == 0 ? 0 : 1));
        ExecutorService executorService = Executors.newFixedThreadPool(spliceCount);
        long startPosition = 0;
        final File file = new File(fileName);
        if(!file.exists()){
            file.createNewFile();
        }
        final CountDownLatch downLatch = new CountDownLatch(spliceCount);
        while(startPosition < fileLength){
            final long start = startPosition;
            long endPosition = startPosition + defSliceCount - 1;
            if(endPosition >= fileLength){
                endPosition = fileLength - 1;
            }
            final long end = endPosition;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    dowmSlice(url,file,start,end,downLatch);
                }
            });
            startPosition = startPosition + defSliceCount;
        }

        downLatch.await();
        System.out.println("下载文件成功!");
    }

    public static void dowmSlice(String url, File file, long startPosition, long endPosition, CountDownLatch downLatch){
        Map<String,String> hardMap = new HashMap<>();
        hardMap.put("Range", createRangeVal(startPosition,endPosition));
        requestGetUrlContent(url, hardMap, new AbstractResponseHandler<Boolean>() {
            @Override
            public Boolean succHandle(HttpResponse response) throws IOException {
                InputStream inputStream = response.getEntity().getContent();
                //创建随机读写类
                RandomAccessFile outputStream = new RandomAccessFile(file, "rw");
                //跳到指定位置
                outputStream.seek(startPosition);
                int count = 0;byte[] buffer=new byte[1024];
                while((count = inputStream.read(buffer, 0, buffer.length))>0){
                    outputStream.write(buffer, 0, count);
                }
                outputStream.close();
                downLatch.countDown();
                return true;
            }
        });
    }

    public static void main(String[] args) throws Exception {
        sliceDown("http://central.maven.org/maven2/log4j/log4j/1.2.16/log4j-1.2.16.jar","D:\\tmp\\log4j-1.2.16.jar");
       /* System.out.println(getConectConentLength("http://central.maven.org/maven2/log4j/log4j/1.2.16/log4j-1.2.16.jar"));
        Map<String,String> hardMap = new HashMap<> ();
        hardMap.put("Range", "bytes=0-1024");
        System.out.println(getConectConentLength("http://central.maven.org/maven2/log4j/log4j/1.2.16/log4j-1.2.16.jar",hardMap));*/

    }

}
